/* 
 * CheckRuleInterceptor.java
 * 
 * Data de criação: 09/04/2010
 * 
 * O2SOLUTIONS - SOLUÇÕES EM TECNOLOGIA DA INFORMAÇÃO. 
 *
 */
package br.com.o2solutions.core.aop.business.rule;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import net.vidageek.mirror.dsl.Mirror;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import br.com.o2solutions.core.aop.business.annotation.ApplyRule;
import br.com.o2solutions.core.aop.business.annotation.IgnoreRules;
import br.com.o2solutions.core.aop.business.annotation.RestrictRules;
import br.com.o2solutions.core.exception.SystemException;
import br.com.o2solutions.core.service.rule.Rule;
import br.com.o2solutions.core.util.RuleUtil;
import br.com.o2solutions.util.assertion.Assert;

/**
 * Interceptador para verificação das anotações que representam as rules a serem
 * aplicadas nas chamadas dos metodos negociais do sistema.
 * 
 * @author osvaldeir
 * @since 09/04/2010
 * 
 */
@Aspect
@Component
public class CheckRuleInterceptor {

	/**
	 * Metodo que intercepta as chamadas aos metodos negociais verificando se
	 * estes possuem a anotacao ApplyRule. Caso exista, sao verificadas quais
	 * validacoes devem ser aplicadas de acordo com as classes que implementam a
	 * interface {@link Rule}.
	 * 
	 * @param pjp
	 *            Objeto referente ao evento ocorrido
	 * @return Object Retorno do metodo
	 * @throws Throwable
	 *             Caso alguma falha ocorra (Sistemica ou Negocial)
	 */
	@Around("execution (@br.com.o2solutions.core.aop.business.annotation.ApplyRule * *(..)) && @within(br.com.o2solutions.core.aop.business.annotation.Business)")
	public Object invokeBusiness(ProceedingJoinPoint pjp) throws Throwable {
		Object result = null;
		MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
		final Method method = methodSignature.getMethod();
		ApplyRule annotationRule = method.getAnnotation(ApplyRule.class);
		final Class<? extends Rule<?>>[] rules = annotationRule.rules();
		final Class<?>[] parameterTypes = method.getParameterTypes();
		Assert.isTrue(
				parameterTypes != null && parameterTypes.length > 0,
				new SystemException(
						"O método ["
								.concat(method
										.getName()
										.concat("] não possui parametros a serem verificados. Remova a annotation @ApplyRule!"))));
		for (Class<? extends Rule<?>> rule : rules) {
			doApplyRule(parameterTypes, pjp.getArgs(), rule, method);
		}
		result = pjp.proceed();
		return result;
	}

	/*
	 * Faz a aplicaçao da Regra em um ou vários argumentos do método.
	 * 
	 * @param arguments
	 * 
	 * @throws CoreException
	 */
	private void doApplyRule(Class<?>[] methodParameters, Object[] arguments,
			Class<? extends Rule<?>> rule, Method method) throws Exception {
		int paramPosition = 0;
		boolean valid = false;
		for (Object object : arguments) {
			if (toApply(rule, methodParameters[paramPosition], object, method,
					paramPosition)) {
				RuleUtil.applyRule(rule, object);
				valid = true;
			}
			++paramPosition;
		}
		Assert.isTrue(
				valid,
				new SystemException(
						"A rule "
								.concat(rule.getName())
								.concat(" não é aplicada a nenhum parametro do método. Remova-a.")));
	}

	/*
	 * Verifica se a Rule possui a referencia ao tipo do objeto a ser aplicada a
	 * Regra e se não há restrições de aplicação.
	 * 
	 * @param rule Regra a ser aplicada
	 * 
	 * @param objectType Tipo do Objeto
	 * 
	 * @param object Objeto a ser aplicada a Rule
	 * 
	 * @return true caso a Rule possua a mesma referencia de tipo ao objeto a
	 * ser aplicada a regra e, caso exista uma restrição, a Rule esteja na lista
	 * de restrições.
	 */
	private boolean toApply(Class<? extends Rule<?>> rule, Class<?> objectType,
			Object object, Method method, int paramPosition) {
		boolean toApply = true;
		final Annotation[][] parameterAnnotations = method
				.getParameterAnnotations();
		final Method ruleMethod = new Mirror().on(rule.getName()).reflectAll()
				.methods().get(0);
		final Class<?> ruleParameter = ruleMethod.getParameterTypes()[0];
		Annotation restrict = extractValidAnnotation(parameterAnnotations,
				paramPosition);
		toApply &= ruleParameter.equals(objectType);
		if (toApply && restrict != null) {
			toApply = !restrict.annotationType().equals(IgnoreRules.class);
			if (toApply) {
				RestrictRules restrictRules = (RestrictRules) (restrict
						.annotationType().equals(RestrictRules.class) ? restrict
						: null);
				toApply &= Arrays.asList(restrictRules.restrictTo()).contains(
						rule);
			}
		}
		return toApply;
	}

	/*
	 * Extrai a Annotation que deve ser utilizada para restrição ou Ignorar a
	 * execução da rule.
	 * 
	 * @param parameters Parametros do método que receberam anotação
	 * 
	 * @param index Posição do Parametro
	 * 
	 * @return Annotation para ignorar ou para restringir o parametro a
	 * aplicação da regra.
	 */
	private Annotation extractValidAnnotation(Annotation[][] parameters,
			int index) {
		Annotation result = null;
		if (parameters[index].length > 0) {
			final List<Annotation> asList = Arrays.asList(parameters[index]);
			final Iterator<Annotation> iterator = asList.iterator();
			int notDuplicated = 0;
			while (iterator.hasNext()) {
				final Annotation next = iterator.next();
				if (next.annotationType().equals(IgnoreRules.class)) {
					result = next;
					notDuplicated++;
				}
				if (next.annotationType().equals(RestrictRules.class)) {
					result = next;
					notDuplicated++;
				}
			}
			Assert.isTrue(
					notDuplicated < 2,
					"As anotacoes IgnoreRules e RestrictRules nao podem ser utilizadas no mesmo parametro");
		}
		return result;
	}

}
